Data Fetching
Use the useFetch hook from the Blotch Library whenever your Widget needs data from an API.
import { useFetch } from "~lib/use-fetch";
type Weather = {
readonly temperature: number;
};
const Widget = () => {
const { data } = useFetch<Weather>("https://api.example.com/weather");
return <div className="text-6xl">{data.temperature}°C</div>;
};
Your Widget waits for the data before it is drawn, so data is always there. There is no loading state to check.
Use useFetch instead of the browser's fetch. Requests go through Blotch's servers, so APIs that block requests from a browser still work.
Typing the response
Pass the shape of the response as the type parameter, and data gets full auto-complete:
type Quote = {
readonly text: string;
readonly author: string;
};
const { data } = useFetch<Quote[]>("https://api.example.com/quotes");
data[0].author; // string
QuickType turns a sample JSON response into a TypeScript type you can paste into your Widget.
Using Inputs
Read your Inputs with useInputs and build them into the URL. Wrap anything a user types in encodeURIComponent, so spaces and symbols survive:
import { useFetch } from "~lib/use-fetch";
import { useInputs } from "~lib/use-inputs";
const Widget = () => {
const { CITY } = useInputs();
const { data } = useFetch<Weather>(
`https://api.example.com/weather?city=${encodeURIComponent(CITY)}`,
);
return <div>{data.temperature}°C</div>;
};
API keys
Never write an API key into your Widget code, because Widget code is public. Store it as a Private Input and send it in a header:
const { API_KEY } = useInputs();
const { data } = useFetch<Weather>("https://api.example.com/weather", {
headers: { "X-Api-Key": API_KEY },
});
For services you sign in to with OAuth, such as Google or Spotify, use an Integration instead. useIntegration gives you an access token to send as an Authorization header.
Sending data
The second argument takes the same options as the browser's fetch, such as method, headers and body:
const { data } = useFetch<SearchResult>("https://api.example.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "weather" }),
});
body can be text, such as JSON, or URLSearchParams or FormData to send a form. Files can't be sent.
Keep each request the same
Your Widget code can run several times while it waits for data, and each response only goes back to the exact request that asked for it. So build the same URL, headers and body every time.
A random ID or the current time comes out different on every run, so its response is never used. After a few tries Blotch stops asking and shows an error in place of your Widget. Make such a value once, outside your Widget:
const DEVICE_ID = Math.random().toString(36).slice(2);
const Widget = () => {
const { data } = useFetch<Session>("https://api.example.com/sessions", {
method: "POST",
body: JSON.stringify({ deviceId: DEVICE_ID }),
});
return <div>Signed in as {data.name}</div>;
};
useState won't keep it either, because React starts your Widget's state over while it waits for its first data.
One request after another
Because data is always there, a second request can use the first one's result:
const { data: user } = useFetch<User>("https://api.example.com/me");
const { data: posts } = useFetch<Post[]>(
`https://api.example.com/users/${user.id}/posts`,
);
Requests run one after another, so each one adds to how long your Widget takes to prepare. Fetch only what you display.
Caching
Pass proxyCache with a ttl in milliseconds, and Blotch keeps the response for that long:
const { data } = useFetch<Weather>("https://api.example.com/weather", {
proxyCache: { ttl: 30 * 60 * 1000 }, // 30 minutes
});
Until the ttl runs out, every identical request, from any Frame, gets the saved response instead of asking the API again. A request is identical when its URL, method, headers and body all match, so every Frame showing London's weather shares one response and Paris gets its own.
Use it when the data changes slowly, such as weather or headlines, or when the API is slow, rate-limited or charges per request. Leave it off for data that must always be current.
- Private requests are never cached. A request with an
Authorization,Proxy-Authorization,CookieorX-Api-Keyheader always goes to the API, because its response may belong to one person. A key in the URL can't be spotted, so only cache responses that are the same for everyone. - Errors are cached too. A shorter
ttlrecovers from a bad response sooner. - The cache can't be cleared early, and a very long
ttlis shortened to the maximum Blotch allows.
Errors
data holds whatever the API sent back, even an error such as 404 Not Found. When the API has an error format, check for it:
type WeatherResponse =
| { readonly temperature: number }
| { readonly error: { readonly message: string } };
const Widget = () => {
const { data } = useFetch<WeatherResponse>("https://api.example.com/weather");
if ("error" in data) {
return <div>Weather unavailable: {data.error.message}</div>;
}
return <div>{data.temperature}°C</div>;
};
If the request can't be made at all, for example because the API is down, Blotch shows the error in place of your Widget. You don't need to handle this yourself.
Retrying
Some APIs answer "still working on it" and expect you to ask again. Give retry a condition that returns true for those responses, and useFetch waits and asks again, up to count times:
const { data } = useFetch<Report>("https://api.example.com/report", {
retry: {
condition: (report) => report.status === "pending",
count: 5,
},
});
Response formats
- JSON is parsed for you.
- XML, RSS and Atom responses that start with
<?xmlbecome plain objects. Elements become keys, and attributes become keys starting with@_, so<rss version="2.0">becomesdata.rss["@_version"]. - Anything else, such as plain text or HTML, arrives as a
string.
If an XML API's responses don't start with <?xml, set output: "force" to convert them anyway. Set output: "pass-through" to keep XML as text.
Reference
import { useFetch } from "~lib/use-fetch";
const { data } = useFetch<T>(url, options);
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL to request. |
options | Options<T> | No | Any fetch option, plus these: |
| Option | Type | Default | Description |
|---|---|---|---|
proxyCache.ttl | number | Not cached | How long to keep the response, in milliseconds. See Caching. |
retry.condition | (data: T) => boolean | No retries | Return true to ask again. See Retrying. |
retry.count | number | 3 | The most times to ask again. |
output | "best-effort" | "force" | "pass-through" | "best-effort" | How XML is converted. See Response formats. |
useFetch returns data, the response body typed as T.